package com.brycen.nio.selector;

import sun.nio.ch.SelectionKeyImpl;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;

/**
 * 问题：数据量大时，执着于一直写的问题
 * 当数据量很大的时候，如何才能提高效率，使线程不在一直循环忙碌与一个客户端的写操作，而是先去干其他客户端的事情等系统底层有空闲来再去写
 * 解决方案：关注可写事件
 */
public class WriteServer {
    public static void main(String[] args) throws IOException {
        //创建ServerSocket通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        //将通道绑定端口
        ssChannel.bind(new InetSocketAddress(9999));
        //配置通道为非阻塞通道
        ssChannel.configureBlocking(false);
        //创建选择器
        Selector selector = Selector.open();
        //将通道与选择器绑定
        //参数1：selector选择器
        //参数2：关注的事件：SelectionKey.OP_ACCEPT：监听注册事件
        //参数3：绑定的附件
        //返回值：SelectionKey：就是将来事件发生后，通过他可以知道是什么事件和哪个channel的事件
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

        //select判断选择器中是否有事件未处理，如果没有则会阻塞；
        //select在事件未处理时不会阻塞，事件发生时要么处理，要么cancel取消，不能置之不理
        while (selector.select()>0){
            //selector.keys()：所有注册的通道
            //selector.selectedKeys()：哪些通道发生了需要处理的事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                //获取事件
                SelectionKey sk = iterator.next();
                //判断事件是否是注册事件
                if (sk.isAcceptable()){
                    //接受客户端连接的通道
                    SocketChannel channel = ssChannel.accept();
                    //配置非阻塞通道
                    channel.configureBlocking(false);
                    SelectionKey scKey = channel.register(selector, SelectionKey.OP_READ);

                    StringBuilder sb = new StringBuilder();
                    for (int i = 0; i < 3000000; i++) {
                        sb.append("a");
                    }
                    ByteBuffer byteBuffer = Charset.defaultCharset().encode(sb.toString());
                    /**
                     * 这里系统有的时候是不能将这么大的数据一次性全部写出的，有时候是需要分批写出的
                     * 返回值就是当前写出的内容长度
                     */
                    int writeLen = channel.write(byteBuffer);
                    System.out.println("当前写出："+writeLen);
                    /**
                     * 判断缓冲区中是否还有剩余数据，如果有则就将该通道的SelectionKey关注可写事件
                     * 这里为什么不用while循环不断往外写，这样也可以，如果写成while那么该线程就一直会执着于将数据全部写出，无论系统是否可以写出
                     * 这样就不能有效的利用空闲去处理其他客户端的事件了
                     */
                    if(byteBuffer.hasRemaining()){
                        //存在数据，将该客户端的channel关注可写事件，下面使用scKey.interestOps()+新事件的形式是保留之前关注的事件
                        //如果直接scKey.interestOps(SelectionKeyImpl.OP_WRITE);会覆盖之前关注的事件
                        scKey.interestOps(scKey.interestOps() + SelectionKeyImpl.OP_WRITE);
                        //将剩余数据的buffer添加到改channel的附件中去
                        scKey.attach(byteBuffer);
                    }

                }else if (sk.isWritable()){//判断是否为可写事件
                    SocketChannel channel = (SocketChannel) sk.channel();
                    ByteBuffer buffer = (ByteBuffer) sk.attachment();
                    int writeLen = channel.write(buffer);
                    System.out.println("可写事件写出："+writeLen);
                    if (!buffer.hasRemaining()){
                        //当缓冲区中没有数据的时候将selector中的SelectionKey添加的附件去除，释放空间
                        sk.attach(null);
                        //取消关注可写事件
                        sk.interestOps(sk.interestOps()-SelectionKeyImpl.OP_WRITE);
                    }
                }
                //移除该事件，否则会重复消费该事件
                iterator.remove();
            }
        }
    }
}
